Odtwarzanie i rejestrowanie dźwięku na niskim poziomie. PARTIII [WinApi][CBuilder]

Krecik

[WinApi][CppBuilder] Odtwarzanie i rejestrowanie dźwięku na niskim poziomie.

PARTIII

Kolejna część artu. Ta dotyczy nagrywania dźwięku na poziomie sampli przy pomocy funkcji WinAPI z rodziny waveIn...</code>, jak <code class="cpp">waveInOpen waveInClose waveInPrepareHeader

//------------------

Nagrywanie dźwięku w systemach Win32 jest tylko odrobinę bardziej skomplikowane niż jego odtwarzanie... Z resztą idea jest ta sama. Tam zapełnialiśmy buffor danymi, a później dawaliśmy go systemowi do odegrania, a tutaj odwrotnie, tworzymy bufor, system go wypełnia, a my później z niego korzystamy.
Jest tylko parę rzeczy więcej do zrobinia, niż przy odtwarzaniu dźwięku.

[OD GÓRY] - czyli co musimy sobie przygotować
Najpierw wstawimy, podaną już w poprzednich częsciach tego kursu, strukturę - przyda nam się później do zapisania nagrania na dysk.

typedef struct _WAVEHEAD {
 struct {
         char            RIFF[4];
         unsigned long   Size;
         char            WAVE[4]; //okresla format pliku
 } RIFF;
 struct {
         char            fmt[4] ;
         unsigned long   BlockSize;
         struct {
           unsigned short  AudioFormat;
           unsigned short  NumChannels;
           unsigned long   SampleRate;
           unsigned long   ByteRate;
           unsigned short  BlockAlign;
           unsigned short  BitsPerSample;
         } Format;
 } fmt;
 struct {
         char            data[4];
         unsigned long   DataSize;
 } data;
} WAVEHEAD;

Teraz deklarujemy kilka globalnch zmiennych i klas:

WAVEFORMATEX    WaveFormat;      // (1
HWAVEIN         WaveHandle;      // (2
WAVEHDR         WaveHeader;      // (3
char*           Buffer = NULL;   // (4
unsigned int    BufferSize = 0;  // (5

I tak po kolei:

    • struktura WAVEFORMATEX - było już o niej trochę w poprzednim artykule. Definiuje ona parametry dźwięku, jego złożoność, jakość itp...
    • HWAVEIN - uchwyt do urządzenia wejściowego z którego korzystamy do nagrania...
    • WAVEHDR - struktura zawierająca informacje o buforze. Mówi gdzie i ile nagrać.
    • Wskaźnik na tablicę (później ją zaalokujemy na stercie operatorem new);
    • Wielkość buforu, wypełnimy to później, przeliczając ile bajtów potrzeba przy danej jakośći na 1 sekundę nagrania...

I w sumie, skoro mamy już deklaracje globalne...

[MOŻEMY ZACZYNAĆ]
Najpierw tworzymy sobie zmienne, które będą opisywały jakość dźwięku. Wykorzystamy je później do wypełnienia struktury WAVEFORMATEX i policzenia rozmiaru buforu. Tak więc:

unsigned short  Channels         = 1;       //kanałów: 1-mono, 2-stereo
unsigned long   SamplesPerSecond = 22050;   //iloc probek na sekunde
unsigned short  BitsPerSample    = 8;       //rozmar probki
unsigned long   RecordSeconds    = 10;      //dlugosc nagrania

I tu chwila wyjśnien...
Channels - chyba nie budzi wątpliwości, określa ilość kanałów nagrywanego dźwięku (zazwyczaj 1 lub 2) ()
SamplesPerSecond - generalnie przyjmuje wartości: 8,000; 11,025; 22,050; 44,100 (
)
BitsPerSample - Wielkość w bitach pojedynczej próbki dźwięku: 8 lub 16 (*)
RecordSeconds - ile sekund dźwięku chcemy nagrać. Za chwilę będziemy używać tej zmiennej do liczenia rozmiaru bufora.

(*) - Mówię tu o podstawowych formatach dźwięku, a z takich teraz korzystamy. Niestandardowe formaty mogą być dostarczane (wraz z dokumentacją), np przez producenta sprzętu.

Rozmiar danych na dysku i wielkość bufora będzie oczywiście zależała od wybranych parametrów jakośći.
I tak rozmiar np. 10 sekund dźwięku nagranego w jakości mono, 8kHz, 8 bitów na próbkę zajmuje około 80kB. Natomiast 10 sekund nagrania stereo, 44.1kHz z 16bitowymi próbkami zajmuje, proporcjonalnie, około 860kB...
Zazwyczaj jednak można ustalić złoty środek... Dźwięki systemu Windows są nagrane z jakością: mono, 22.05kHz, 8bitów - jest to kompromis między wielkością, a jakością dźwięku.

Teraz, kiedy określiliśmy sobie z jaką jakośćią chcemy nasz music nagrywać, wypełniamy stosownymi danychmi strukturę WAVEFORMATEX...

WaveFormat.wFormatTag      = WAVE_FORMAT_PCM;
WaveFormat.nChannels       = Channels;
WaveFormat.nSamplesPerSec  = SamplesPerSecond;
WaveFormat.wBitsPerSample  = BitsPerSample;
WaveFormat.nAvgBytesPerSec = SamplesPerSecond * Channels;
WaveFormat.nBlockAlign     = (Channels*BitsPerSample)/8;
WaveFormat.cbSize          = 0;

Ostatnia wartość (cbSize) - dookreśla pewne informacje (a w zasadzie ich rozmiar) niestandardowych formatów, jednak nas to na razie.

Ufff...
Teraz należy sprawdzić, czy nasz sprzęt poradzi sobie z wymogami, jakie mu postawiliśmy.
Poprostu fikcyjnie (ostatni argument WAVE_FORMAT_QUERY) próbujemy otworzyć nasze urządzenie w danych parametrach audio.
Funkcja nie robi nic, a tylko zwraca ewentualnie WAVEERR_BADFORMAT - kiedy sprzęt nie obsługuje formatu.

//sprawdzamy, czy sprzęt obsługuje taki format
int Res = waveInOpen(&WaveHandle, WAVE_MAPPER, &WaveFormat, 0, 0, WAVE_FORMAT_QUERY);
if (Res == WAVERR_BADFORMAT) return;

A jeśli się jednak uda (na co oczywiście liczymy) naprawdę otwieramy urządzenie (WAVE_MAPPER - trochę bardziej omówiony w poprzedniej częsci atru).
Funkcja przypisuje do WaveHandle uchwyt wejściwoego urządzenia audio.

//otwieramy device  (WAVE_MAPPER = standardowe urządzenie dźwięku [pierwszy przetwornik Cyfrowo->Analogowy])
Res = waveInOpen(&WaveHandle, WAVE_MAPPER, &WaveFormat, (MAKELONG(Handle,0)), 0, CALLBACK_WINDOW);
if(Res) {/*tu mozna machnac jakis komunikat o bledzie, wiecej informacji w zemiennej Res*/; return;}

[NIECH SIĘ STANIE BUFOR] - czyli przygotowujemy miejsce na nagranie
Teraz trzeba przygotować bufor na dane... Czyli ustalamy jego rozmiar i alokujemy w pamięci.

//przygotowujemy bufor na nagranie
BufferSize = RecordSeconds * (BitsPerSample / 8) * SamplesPerSecond * Channels;
Buffer = new char [BufferSize];

Wielkość buforu jest ustalana przez mnożenie: ilości próbek na sekundę, ilości bajtów na próbkę, ilości kanałów i sekund do nagrania...
Chyba oczywiste dlaczego...

Następnie przygotowujemy klasę WAVEHDR zawierającą informacje o naszym buforze...

//przygotowujemy klase WAVEHDR zawierajaca informacje potrzebne windowsowi... [m.in. rozmiar i pointer do buforu]
WaveHeader.dwBufferLength  = BufferSize;
WaveHeader.dwFlags         = 0;
WaveHeader.lpData          = Buffer;
//przygotowujemy nagłówek:
Res = waveInPrepareHeader(WaveHandle, &WaveHeader, sizeof(WAVEHDR));
if(Res){/*komunikat o błędzie*/; if(Buffer) delete Buffer; return;}

Co tu tłumaczyć... Wypełniay podstawowe elementy struktury informacjami o buforze.
A funkcja waveInPrepareHeader przygotowuje klase WAVEHDR w oparciu o handle do urządzenia.

Teraz dodajemy bufor do listy buforów (my używamy jednego, ale ten krok musimy wykonac)

Res = waveInAddBuffer(WaveHandle, &WaveHeader, sizeof(WAVEHDR));

[1,2,3... PR|Ó|BA MIKROFONU]
Rozpoczęcie nagrywania, jest bardzo przyjemną rzaczą, bo jest bardzo proste...
Po prostu wywołujemy funkcję:

//zaczynamy nagrywanie dzwieku
Res = waveInStart(WaveHandle);
if(Res){/*komunikat o niepowodzeniu*/; if(Buffer) delete Buffer; return;}

W sumie, już skończyliśmy :P Ale wypadałoby jeszcze coś z nagranymi danymi zrobić... Zapisać, czy coś. I zwolnić pamięć.
Tu z pomocą przychodzi nam CallBack od Windowsa. Do okna, które wywołałó funckje nagrywania są wysyłąne m.in. następujące komunikaty:
MM_WIM_OPEN Otwarcie sprzętu
MM_WIM_CLOSE Zamknięcie go...
MM_WIM_DATA Zakończenie nagrywania. Zwrócenie bufforu do porogramu.

Oczywiście nas najbardziej interesował będzie komunikat ostatni... W zasadzie musimy ten komunikat odebrać, by móc cokolwiek zrobić z nagranym dźwiękiem.

Tak więc zasadzamy się na ten komunikat (Borland Builder):

BEGIN_MESSAGE_MAP
 MESSAGE_HANDLER(MM_WIM_DATA, TMessage, OnWaveMessage)
END_MESSAGE_MAP(TForm)

I piszemy funkcję do jego odebrania...

void TForm1::OnWaveMessage(TMessage& msg)
{
if(msg.Msg == MM_WIM_DATA)
 {
 //zamykamy sprzęt
 waveInClose(WaveHandle);
 //zapisujemy plik
 SaveWaveFile("c:\\plik.wav");
 //no i... zwalniamy pamiec
 WaveHeader.lpData = NULL;
 delete[] Buffer;
 Buffer = NULL;
 }
}

Czyli co widać, to jest. Zamykamy urządzenie, zapisujemy do pliku i zwalniamy Bufor.

Teraz została do oprogramowanie ostatnia na dzisiaj funkcja... SaveWaveFile. Wygląda mniej~więcej tak:

#include <stdio.h>
void TForm1::SaveWaveFile(char* FileName)
{
//operacja w sumie podobna  do odczytu dźwieku, ino w drugą stronę...
WAVEHEAD  WaveHead; //nagłówek pliku
WaveHead.RIFF.Size      = 36 + BufferSize;
WaveHead.fmt.BlockSize  = 16;
WaveHead.data.DataSize  = BufferSize;
memcpy(&(WaveHead.fmt.Format),&WaveFormat,sizeof(WAVEFORMATEX));
four(WaveHead.RIFF.RIFF,"RIFF");
four(WaveHead.RIFF.WAVE,"WAVE");
four(WaveHead.fmt.fmt  ,"fmt ");
four(WaveHead.data.data,"data");

//i do pliku z tym
FILE *plik = fopen(FileName,"w"); //nadpisujemy plik, jesli istnial
fwrite(&WaveHead,sizeof(WAVEHEAD),1,plik);
fwrite(Buffer,BufferSize,1,plik);
fclose(plik);
}

Jak widać bardzo podobna fuckcja do odczytującej.
Wypełniamy kolejne struktury (liczby 36, 16 określają deltę, stałe związki między pewnymi danymi przy zapisie).
Tu się muszę troszeczkę wytłumaczyć... Ta funkcja działa poprawnie tylko przypadkiem - to znaczy działa dlatego, że działamy na podstawowym formacie dźwięku, który nie zawiera dodatkowych informacji. Prawidłowe działanie tej funkcji zapewnia wypełnianie odpowiednich pól w odpowiedniej kolejności. Wtedy dane trafią na swoje miejsca i wszytko będzie dobrze chodziło. Czyli funkcja działa i ma się dobrze, ale trzeba pamiętać, że jest trochę niedopracowana (sorry, to moje lenistwo). W każdym razie nie ma co się przejmować, bo i tak wszystkie podane tu kody są przewidziane na działanie na standardowych formatach. Czyli spox ;)

Wypełniamy pola WAVEHEAD z WAFEROMATEX, zapisujemy do pliku i zapisujemy tam też bufor.

To już koniec. Powinno działać - a przynajmniej mam taką nadzieję.

Piotr Topa - [Krecik]

2 komentarzy

Wielkie dzięki. Naprawdę kawał dobrej roboty. I bardzo (przynajmniej dla mnie) przydatnej :) Jeszcze raz wielkie dzięki :)

Hej. Nie chce mi działać funkcja 'four'. W jakiej bibliotece można ją znaleźć ?